1 /**
2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 This file contains the command line parsing and configuration loading from file.
7 
8 The data flow is to first load the configuration from the file then parse the command line.
9 This allows the user to override the configuration via the CLI.
10 */
11 module code_checker.cli;
12 
13 import logger = std.experimental.logger;
14 import std.array : array, empty, appender;
15 import std.exception : collectException, ifThrown;
16 import std.path : buildPath, dirName;
17 import std.typecons : Tuple, Flag;
18 
19 import toml : TOMLDocument, TOMLValue;
20 import my.path : AbsolutePath, Path;
21 
22 @safe:
23 
24 enum AppMode {
25     none,
26     help,
27     helpUnknownCommand,
28     normal,
29     initConfig,
30 }
31 
32 /// Configuration options only relevant for static code checkers.
33 struct ConfigStaticCode {
34     import code_checker.engine.types : Severity;
35 
36     /// Filter results from analyzers on this severity.
37     Severity severity;
38 
39     /// Analyzers to use.
40     string[] analyzers = ["clang-tidy"];
41 
42     /// Files matching this pattern should not be analyzed.
43     string[] fileExcludeFilter;
44 }
45 
46 /// Configuration options only relevant for clang-tidy.
47 struct ConfigClangTidy {
48     /// System configuration to use if .clang-tidy do not exists in work directory.
49     string systemConfig = "{code_checker}/../etc/code_checker/clang_tidy.conf";
50 
51     /// Checks to toggle on/off. Used as a compliment to checks.
52     string[] checkExtensions;
53 
54     /// Used as a compliment to options. options is set in the global while optionExtensions is set locally.
55     string[string] optionExtensions;
56 
57     /// Argument to the be passed on to clang-tidy's --header-filter paramter as-is
58     string headerFilter;
59 
60     /// Apply fix hints.
61     bool applyFixit;
62 
63     /// Apply fix hints even though they result in errors.
64     bool applyFixitErrors;
65 
66     /// The clang-tidy binary to use.
67     string binary = "clang-tidy";
68 }
69 
70 /// Configuration options only relevant for iwyu.
71 struct ConfigIwyu {
72     /// The clang-tidy binary to use.
73     string binary = "iwyu";
74 
75     /// Extra args to pass on to iwyu.
76     string[] extraFlags;
77 
78     /// Map files to pass on to iwyu.
79     string[] maps;
80 
81     /// Map files to pass on to iwyu.
82     string[] defaultMaps;
83 }
84 
85 /// Configuration data for the compile_commands.json
86 struct ConfigCompileDb {
87     import compile_db : CompileCommandFilter;
88 
89     /// Command to generate the compile_commands.json
90     string generateDb;
91 
92     /// Dependencies that the generate command have. If it is set then the
93     /// commmand is only executed when they are changed.
94     AbsolutePath[] generateDbDeps;
95 
96     /// Raw user input via either config or cli
97     string[] rawDbs;
98 
99     /// Either a path to a compilation database or a directory to search for one in.
100     AbsolutePath[] dbs;
101 
102     /// Flags the user wants to be automatically removed from the compile_commands.json.
103     CompileCommandFilter flagFilter;
104 
105     /// If files should be deduplicated thus only unique files are analyzed.
106     bool dedupFiles;
107 }
108 
109 /// Settings for the compiler
110 struct Compiler {
111     import compile_db : SystemCompiler = Compiler;
112 
113     /// Additional flags the user wants to add besides those that are in the compile_commands.json.
114     string[] extraFlags;
115 
116     /// Deduce compiler flags from this compiler and not the one in the
117     /// supplied compilation database.  / This is needed when the one specified
118     /// in the DB has e.g. a c++ stdlib that is not compatible with clang.
119     SystemCompiler useCompilerSystemIncludes;
120 }
121 
122 /// Settings for logging.
123 struct Logging {
124     import colorlog : VerboseMode;
125 
126     VerboseMode verbose;
127 
128     /// If logging to files should be done.
129     bool toFile;
130 
131     /// Directory to log to.
132     AbsolutePath dir;
133 }
134 
135 /// Configuration of how to use the program.
136 struct Config {
137     AppMode mode;
138 
139     /// Where the base configurations are stored.
140     AbsolutePath baseConfDir;
141 
142     /// System configuration.
143     AbsolutePath systemConfDir;
144 
145     /// Name of the base configuration to merge with the users.
146     string baseConfName = "default";
147 
148     /// Path to the base configuration that the user wants to use.
149     AbsolutePath baseUserConf() @safe const {
150         return buildPath(baseConfDir, "code_checker_" ~ baseConfName ~ ".toml").Path.AbsolutePath;
151     }
152 
153     /// Working directory as specified by the user.
154     AbsolutePath workDir;
155 
156     /// Configuration file as specified by the user or the default one.
157     AbsolutePath confFile;
158 
159     AbsolutePath database;
160 
161     ConfigClangTidy clangTidy;
162     ConfigCompileDb compileDb;
163     ConfigIwyu iwyu;
164     ConfigStaticCode staticCode;
165 
166     Compiler compiler;
167     Logging logg;
168 
169     /// If set then only analyze these files.
170     AbsolutePath[] analyzeFiles;
171 
172     /// Returns: a config object with default values.
173     static Config make(AbsolutePath workDir, AbsolutePath confFile) @safe {
174         import std.file : thisExePath;
175         import std.process : environment;
176         import compile_db : defaultCompilerFlagFilter, CompileCommandFilter;
177 
178         Config c;
179         c.workDir = workDir;
180         c.confFile = confFile;
181         c.compileDb.flagFilter = CompileCommandFilter(defaultCompilerFlagFilter, 0);
182         c.baseConfDir = environment.get("CODE_CHECKER_DEFAULT",
183                 buildPath(thisExePath.dirName, "..")).Path.AbsolutePath;
184         c.systemConfDir = AbsolutePath(c.baseConfDir ~ "etc/code_checker");
185 
186         return c;
187     }
188 }
189 
190 /// Minimal config to setup path to config file and workdir.
191 struct MiniConfig {
192     /// Value from the user via CLI, unmodified.
193     string rawWorkDir;
194 
195     /// Converted to an absolute path.
196     AbsolutePath workDir;
197 
198     /// Value from the user via CLI, unmodified.
199     string rawConfFile = ".code_checker.toml";
200 
201     /// The configuration file that has been loaded
202     AbsolutePath confFile;
203 }
204 
205 /// Returns: minimal config to load settings and setup working directory.
206 MiniConfig parseConfigCLI(string[] args) @trusted nothrow {
207     import std.file : getcwd;
208     import std.path : dirName;
209     static import std.getopt;
210 
211     MiniConfig conf;
212 
213     try {
214         std.getopt.getopt(args, std.getopt.config.keepEndOfOptions, std.getopt.config.passThrough,
215                 "workdir", "none not visible to the user", &conf.rawWorkDir,
216                 "c|config", "none not visible to the user", &conf.rawConfFile);
217         conf.confFile = Path(conf.rawConfFile).AbsolutePath;
218         if (conf.rawWorkDir.length == 0) {
219             conf.rawWorkDir = getcwd;
220         }
221         conf.workDir = Path(conf.rawWorkDir).AbsolutePath;
222     } catch (Exception e) {
223         logger.error("Invalid cli values: ", e.msg).collectException;
224         logger.trace(conf).collectException;
225     }
226 
227     return conf;
228 }
229 
230 void parseCLI(string[] args, ref Config conf) @trusted {
231     import std.algorithm : map, among, filter;
232     import std.format : format;
233     import std.path : dirName, buildPath;
234     import std.traits : EnumMembers;
235     import code_checker.engine.types : Severity;
236     import colorlog : VerboseMode;
237     static import std.getopt;
238 
239     bool verbose_info;
240     bool verbose_trace;
241     std.getopt.GetoptResult help_info;
242     try {
243         bool init_conf;
244         string config_file = ".code_checker.toml";
245         string database = "code_checker.sqlite3";
246         string logdir = ".";
247         string workdir;
248         string[] analyze_files;
249         string[] analyzers;
250         string[] compile_dbs;
251         string[] src_filter;
252 
253         // dfmt off
254         help_info = std.getopt.getopt(args,
255             "a|analyzer", "Analysers to run", &analyzers,
256             "clang-tidy-bin", "clang-tidy binary to use", &conf.clangTidy.binary,
257             "clang-tidy-fix", "apply suggested clang-tidy fixes", &conf.clangTidy.applyFixit,
258             "clang-tidy-fix-errors", "apply suggested clang-tidy fixes even if they result in compilation errors", &conf.clangTidy.applyFixitErrors,
259             "compile-db", "path to a compilationi database or where to search for one", &compile_dbs,
260             "c|config", "load configuration (default: .code_checker.toml)", &config_file,
261             "db|database", "Database path", &database,
262             "f|file", "if set then analyze only these files (default: all)", &analyze_files,
263             "init", "create an initial config to use", &init_conf,
264             "init-template", "base the initial config on the named template (default: default)", &conf.baseConfName,
265             "iwyu-bin", "iwyu binary to use", &conf.iwyu.binary,
266             "iwyu-map", "give iwyu one or more mapping files", &conf.iwyu.maps,
267             "log", "create a logfile for each analyzed file", &conf.logg.toFile,
268             "logdir", "path to create logfiles in (default: .)", &logdir,
269             "severity", format("report issues with a severity >= to this value (default: style) %s", [EnumMembers!Severity]), &conf.staticCode.severity,
270             "v|verbose", format("verbose mode is set to trace (%-(%s,%))", [EnumMembers!VerboseMode]), &conf.logg.verbose,
271             "workdir", "use this path as the working directory when programs used by analyzers are executed (default: .)", &workdir,
272             );
273         // dfmt on
274         conf.mode = AppMode.normal;
275         if (help_info.helpWanted)
276             conf.mode = AppMode.help;
277         else if (init_conf)
278             conf.mode = AppMode.initConfig;
279 
280         // use a sane default which is to look in the current directory
281         if (compile_dbs.length == 0 && conf.compileDb.dbs.length == 0) {
282             compile_dbs = ["./compile_commands.json"];
283         } else if (compile_dbs.length != 0) {
284             conf.compileDb.rawDbs = compile_dbs;
285         }
286 
287         if (!analyzers.empty)
288             conf.staticCode.analyzers = analyzers;
289 
290         if (conf.logg.toFile)
291             conf.logg.dir = Path(logdir).AbsolutePath;
292 
293         conf.database = AbsolutePath(database);
294 
295         // dfmt off
296         conf.compileDb.dbs = conf
297             .compileDb.rawDbs
298             .filter!(a => a.length != 0)
299             .map!(a => Path(buildPath(conf.workDir, a)).AbsolutePath)
300             .array;
301         // dfmt on
302 
303         conf.analyzeFiles = analyze_files.map!(a => Path(buildPath(conf.workDir,
304                 a)).AbsolutePath).array;
305     } catch (std.getopt.GetOptException e) {
306         // unknown option
307         logger.error(e.msg);
308         conf.mode = AppMode.helpUnknownCommand;
309     } catch (Exception e) {
310         logger.error(e.msg);
311         conf.mode = AppMode.helpUnknownCommand;
312     }
313 
314     void printHelp() @trusted {
315         import std.getopt : defaultGetoptPrinter;
316         import std.format : format;
317         import std.path : baseName;
318 
319         defaultGetoptPrinter(format("usage: %s\n", args[0].baseName), help_info.options);
320     }
321 
322     if (conf.mode.among(AppMode.help, AppMode.helpUnknownCommand)) {
323         printHelp;
324         return;
325     }
326 }
327 
328 /** Load the configuration from file.
329  *
330  * Example of a TOML configuration
331  * ---
332  * [defaults]
333  * check_name_standard = true
334  * ---
335  */
336 void loadConfig(ref Config rval, string configFile) @trusted {
337     import std.algorithm : map;
338     import std.file : exists, readText;
339     import std.path : dirName, buildPath;
340     import toml;
341 
342     if (!exists(configFile))
343         return;
344 
345     static auto tryLoading(string configFile) {
346         auto txt = readText(configFile);
347         auto doc = parseTOML(txt);
348         return doc;
349     }
350 
351     TOMLDocument doc;
352     try {
353         doc = tryLoading(configFile);
354     } catch (Exception e) {
355         logger.warning("Unable to read the configuration from ", configFile);
356         logger.warning(e.msg);
357         return;
358     }
359 
360     alias Fn = void delegate(ref Config c, ref TOMLValue v);
361     Fn[string] callbacks;
362 
363     void defaults__check_name_standard(ref Config c, ref TOMLValue v) {
364         import std.traits : EnumMembers;
365         import code_checker.engine.types : toSeverity, Severity;
366 
367         auto s = toSeverity(v.str);
368         if (s.isNull) {
369             logger.warningf("Unknown severity level %s. Using default: style", v.str);
370             logger.warningf("valid values are: %s", [EnumMembers!Severity]);
371             c.staticCode.severity = Severity.style;
372         } else {
373             c.staticCode.severity = s.get;
374         }
375     }
376 
377     callbacks["defaults.severity"] = &defaults__check_name_standard;
378     callbacks["defaults.analyzers"] = (ref Config c, ref TOMLValue v) {
379         c.staticCode.analyzers = v.array.map!"a.str".array;
380     };
381 
382     callbacks["compile_commands.search_paths"] = (ref Config c, ref TOMLValue v) {
383         c.compileDb.rawDbs = v.array.map!"a.str".array;
384     };
385     callbacks["compile_commands.generate_cmd"] = (ref Config c, ref TOMLValue v) {
386         c.compileDb.generateDb = v.str;
387     };
388     callbacks["compile_commands.generate_cmd_deps"] = (ref Config c, ref TOMLValue v) {
389         try {
390             c.compileDb.generateDbDeps = v.array
391                 .map!"a.str"
392                 .map!(a => AbsolutePath(a))
393                 .array;
394         } catch (Exception e) {
395             logger.warning(e.msg);
396         }
397     };
398     callbacks["compile_commands.exclude"] = (ref Config c, ref TOMLValue v) {
399         c.staticCode.fileExcludeFilter = v.array.map!"a.str".array;
400     };
401     callbacks["compile_commands.filter"] = (ref Config c, ref TOMLValue v) {
402         import compile_db : FilterClangFlag;
403 
404         c.compileDb.flagFilter.filter = v.array.map!(a => FilterClangFlag(a.str)).array;
405     };
406     callbacks["compile_commands.skip_compiler_args"] = (ref Config c, ref TOMLValue v) {
407         c.compileDb.flagFilter.skipCompilerArgs = cast(int) v.integer;
408     };
409     callbacks["compile_commands.dedup"] = (ref Config c, ref TOMLValue v) {
410         c.compileDb.dedupFiles = v == true;
411     };
412 
413     callbacks["clang_tidy.binary"] = (ref Config c, ref TOMLValue v) {
414         c.clangTidy.binary = v.str;
415     };
416     callbacks["clang_tidy.header_filter"] = (ref Config c, ref TOMLValue v) {
417         c.clangTidy.headerFilter = v.str;
418     };
419     callbacks["clang_tidy.checks"] = (ref Config c, ref TOMLValue v) {
420         logger.warning("clang_tidy.checks is deprecated. It is replaced by ",
421                 c.clangTidy.systemConfig);
422     };
423     callbacks["clang_tidy.check_extensions"] = (ref Config c, ref TOMLValue v) {
424         c.clangTidy.checkExtensions = v.array.map!(a => a.str).array;
425     };
426     callbacks["clang_tidy.options"] = (ref Config c, ref TOMLValue v) {
427         logger.warning("clang_tidy.options is deprecated. It is replaced by ",
428                 c.clangTidy.systemConfig);
429     };
430     callbacks["clang_tidy.option_extensions"] = (ref Config c, ref TOMLValue v) {
431         // dummo to suppress warning about unknown key
432     };
433     callbacks["clang_tidy.system_config"] = (ref Config c, ref TOMLValue v) {
434         c.clangTidy.systemConfig = v.str;
435     };
436 
437     callbacks["compiler.extra_flags"] = (ref Config c, ref TOMLValue v) {
438         c.compiler.extraFlags = v.array.map!(a => a.str).array;
439     };
440     callbacks["compiler.use_compiler_system_includes"] = (ref Config c, ref TOMLValue v) {
441         c.compiler.useCompilerSystemIncludes = v.str;
442     };
443 
444     callbacks["iwyu.binary"] = (ref Config c, ref TOMLValue v) {
445         c.iwyu.binary = v.str;
446     };
447     callbacks["iwyu.flags"] = (ref Config c, ref TOMLValue v) {
448         c.iwyu.extraFlags = v.array.map!(a => a.str).array;
449     };
450     callbacks["iwyu.mapping_files"] = (ref Config c, ref TOMLValue v) {
451         c.iwyu.maps = v.array.map!(a => a.str).array;
452     };
453     callbacks["iwyu.default_mapping_files"] = (ref Config c, ref TOMLValue v) {
454         c.iwyu.defaultMaps = v.array.map!(a => a.str).array;
455     };
456 
457     void iterSection(ref Config c, string sectionName) {
458         if (auto section = sectionName in doc) {
459             // specific configuration from section members
460             foreach (k, v; *section) {
461                 if (auto cb = sectionName ~ "." ~ k in callbacks)
462                     (*cb)(c, v);
463                 else
464                     logger.infof("Unknown key '%s' in configuration section '%s'", k, sectionName);
465             }
466         }
467     }
468 
469     iterSection(rval, "defaults");
470     iterSection(rval, "compile_commands");
471     iterSection(rval, "compiler");
472     iterSection(rval, "clang_tidy");
473     iterSection(rval, "iwyu");
474 
475     if (auto section = "clang_tidy" in doc)
476         rval.clangTidy.optionExtensions = parseDict(*section, "option_extensions");
477 }
478 
479 string[string] parseDict(ref TOMLValue root, string section) @trusted {
480     import toml;
481 
482     typeof(return) rval;
483 
484     if (section !in root)
485         return rval;
486 
487     foreach (k, s; *(section in root)) {
488         try {
489             rval[k] = s.str;
490         } catch (Exception e) {
491             logger.warningf("error in %s: %s", section, e.msg);
492         }
493     }
494 
495     return rval;
496 }